/*
  ==============================================================================

   This file is part of the JUCE framework.
   Copyright (c) Raw Material Software Limited

   JUCE is an open source framework subject to commercial or open source
   licensing.

   By downloading, installing, or using the JUCE framework, or combining the
   JUCE framework with any other source code, object code, content or any other
   copyrightable work, you agree to the terms of the JUCE End User Licence
   Agreement, and all incorporated terms including the JUCE Privacy Policy and
   the JUCE Website Terms of Service, as applicable, which will bind you. If you
   do not agree to the terms of these agreements, we will not license the JUCE
   framework to you, and you must discontinue the installation or download
   process and cease use of the JUCE framework.

   JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
   JUCE Privacy Policy: https://juce.com/juce-privacy-policy
   JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/

   Or:

   You may also use this code under the terms of the AGPLv3:
   https://www.gnu.org/licenses/agpl-3.0.en.html

   THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
   WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
   MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.

  ==============================================================================
*/

namespace juce
{

namespace CDReaderHelpers
{

#define FILE_ANY_ACCESS 0
#ifndef FILE_READ_ACCESS
 #define FILE_READ_ACCESS 1
#endif
#ifndef FILE_WRITE_ACCESS
 #define FILE_WRITE_ACCESS 2
#endif

#define METHOD_BUFFERED 0
#define IOCTL_SCSI_BASE 4
#define SCSI_IOCTL_DATA_OUT          0
#define SCSI_IOCTL_DATA_IN           1
#define SCSI_IOCTL_DATA_UNSPECIFIED  2

#define CTL_CODE2(DevType, Function, Method, Access) (((DevType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))
#define IOCTL_SCSI_PASS_THROUGH_DIRECT  CTL_CODE2( IOCTL_SCSI_BASE, 0x0405, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS )
#define IOCTL_SCSI_GET_ADDRESS          CTL_CODE2( IOCTL_SCSI_BASE, 0x0406, METHOD_BUFFERED, FILE_ANY_ACCESS )

#define SENSE_LEN         14
#define SRB_ENABLE_RESIDUAL_COUNT 0x04
#define SRB_DIR_IN        0x08
#define SRB_DIR_OUT       0x10
#define SRB_EVENT_NOTIFY  0x40
#define SC_HA_INQUIRY     0x00
#define SC_GET_DEV_TYPE   0x01
#define SC_EXEC_SCSI_CMD  0x02
#define SS_PENDING        0x00
#define SS_COMP           0x01
#define SS_ERR            0x04

enum
{
    READTYPE_ANY = 0,
    READTYPE_ATAPI1 = 1,
    READTYPE_ATAPI2 = 2,
    READTYPE_READ6 = 3,
    READTYPE_READ10 = 4,
    READTYPE_READ_D8 = 5,
    READTYPE_READ_D4 = 6,
    READTYPE_READ_D4_1 = 7,
    READTYPE_READ10_2 = 8
};

struct SCSI_PASS_THROUGH
{
    USHORT Length;
    UCHAR ScsiStatus;
    UCHAR PathId;
    UCHAR TargetId;
    UCHAR Lun;
    UCHAR CdbLength;
    UCHAR SenseInfoLength;
    UCHAR DataIn;
    ULONG DataTransferLength;
    ULONG TimeOutValue;
    ULONG DataBufferOffset;
    ULONG SenseInfoOffset;
    UCHAR Cdb[16];
};

struct SCSI_PASS_THROUGH_DIRECT
{
    USHORT Length;
    UCHAR ScsiStatus;
    UCHAR PathId;
    UCHAR TargetId;
    UCHAR Lun;
    UCHAR CdbLength;
    UCHAR SenseInfoLength;
    UCHAR DataIn;
    ULONG DataTransferLength;
    ULONG TimeOutValue;
    PVOID DataBuffer;
    ULONG SenseInfoOffset;
    UCHAR Cdb[16];
};

struct SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER
{
    SCSI_PASS_THROUGH_DIRECT spt;
    ULONG Filler;
    UCHAR ucSenseBuf[32];
};

struct SCSI_ADDRESS
{
    ULONG Length;
    UCHAR PortNumber;
    UCHAR PathId;
    UCHAR TargetId;
    UCHAR Lun;
};

#pragma pack(1)

struct SRB_GDEVBlock
{
    BYTE SRB_Cmd;
    BYTE SRB_Status;
    BYTE SRB_HaID;
    BYTE SRB_Flags;
    DWORD SRB_Hdr_Rsvd;
    BYTE SRB_Target;
    BYTE SRB_Lun;
    BYTE SRB_DeviceType;
    BYTE SRB_Rsvd1;
    BYTE pad[68];
};


struct SRB_ExecSCSICmd
{
    BYTE SRB_Cmd;
    BYTE SRB_Status;
    BYTE SRB_HaID;
    BYTE SRB_Flags;
    DWORD SRB_Hdr_Rsvd;
    BYTE SRB_Target;
    BYTE SRB_Lun;
    WORD SRB_Rsvd1;
    DWORD SRB_BufLen;
    BYTE *SRB_BufPointer;
    BYTE SRB_SenseLen;
    BYTE SRB_CDBLen;
    BYTE SRB_HaStat;
    BYTE SRB_TargStat;
    VOID *SRB_PostProc;
    BYTE SRB_Rsvd2[20];
    BYTE CDBByte[16];
    BYTE SenseArea[SENSE_LEN + 2];
};

struct SRB
{
    BYTE SRB_Cmd;
    BYTE SRB_Status;
    BYTE SRB_HaId;
    BYTE SRB_Flags;
    DWORD SRB_Hdr_Rsvd;
};

struct TOCTRACK
{
    BYTE rsvd;
    BYTE ADR;
    BYTE trackNumber;
    BYTE rsvd2;
    BYTE addr[4];
};

struct TOC
{
    WORD tocLen;
    BYTE firstTrack;
    BYTE lastTrack;
    TOCTRACK tracks[100];
};

#pragma pack()

//==============================================================================
struct CDDeviceDescription
{
    CDDeviceDescription()  : ha (0), tgt (0), lun (0), scsiDriveLetter (0)
    {
    }

    void createDescription (const char* data)
    {
        description << String (data + 8, 8).trim() // vendor
                    << ' ' << String (data + 16, 16).trim() // product id
                    << ' ' << String (data + 32, 4).trim(); // rev
    }

    String description;
    BYTE ha, tgt, lun;
    char scsiDriveLetter; // will be 0 if not using scsi
};

//==============================================================================
class CDReadBuffer
{
public:
    CDReadBuffer (const int numberOfFrames)
        : startFrame (0), numFrames (0), dataStartOffset (0),
          dataLength (0), bufferSize (2352 * numberOfFrames), index (0),
          buffer (bufferSize), wantsIndex (false)
    {
    }

    bool isZero() const noexcept
    {
        for (int i = 0; i < dataLength; ++i)
            if (buffer [dataStartOffset + i] != 0)
                return false;

        return true;
    }

    int startFrame, numFrames, dataStartOffset;
    int dataLength, bufferSize, index;
    HeapBlock<BYTE> buffer;
    bool wantsIndex;
};

class CDDeviceHandle;

//==============================================================================
class CDController
{
public:
    CDController() : initialised (false) {}
    virtual ~CDController() {}

    virtual bool read (CDReadBuffer&) = 0;
    virtual void shutDown() {}

    bool readAudio (CDReadBuffer& rb, CDReadBuffer* overlapBuffer = 0);
    int getLastIndex();

public:
    CDDeviceHandle* deviceInfo;
    int framesToCheck, framesOverlap;
    bool initialised;

    void prepare (SRB_ExecSCSICmd& s);
    void perform (SRB_ExecSCSICmd& s);
    void setPaused (bool paused);
};


//==============================================================================
class CDDeviceHandle
{
public:
    CDDeviceHandle (const CDDeviceDescription& device, HANDLE scsiHandle_)
        : info (device), scsiHandle (scsiHandle_), readType (READTYPE_ANY)
    {
    }

    ~CDDeviceHandle()
    {
        if (controller != nullptr)
        {
            controller->shutDown();
            controller = 0;
        }

        if (scsiHandle != 0)
            CloseHandle (scsiHandle);
    }

    bool readTOC (TOC* lpToc);
    bool readAudio (CDReadBuffer& buffer, CDReadBuffer* overlapBuffer = 0);
    void openDrawer (bool shouldBeOpen);
    void performScsiCommand (HANDLE event, SRB_ExecSCSICmd& s);

    CDDeviceDescription info;
    HANDLE scsiHandle;
    BYTE readType;

private:
    std::unique_ptr<CDController> controller;

    bool testController (int readType, CDController* newController, CDReadBuffer& bufferToUse);
};

//==============================================================================
HANDLE createSCSIDeviceHandle (const char driveLetter)
{
    TCHAR devicePath[] = { L'\\', L'\\', L'.', L'\\', static_cast<TCHAR> (driveLetter), L':', 0, 0 };
    DWORD flags = GENERIC_READ | GENERIC_WRITE;
    HANDLE h = CreateFile (devicePath, flags, FILE_SHARE_WRITE | FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);

    if (h == INVALID_HANDLE_VALUE)
    {
        flags ^= GENERIC_WRITE;
        h = CreateFile (devicePath, flags, FILE_SHARE_WRITE | FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
    }

    return h;
}

void findCDDevices (Array<CDDeviceDescription>& list)
{
    for (char driveLetter = 'b'; driveLetter <= 'z'; ++driveLetter)
    {
        TCHAR drivePath[] = { static_cast<TCHAR> (driveLetter), L':', L'\\', 0, 0 };

        if (GetDriveType (drivePath) == DRIVE_CDROM)
        {
            HANDLE h = createSCSIDeviceHandle (driveLetter);

            if (h != INVALID_HANDLE_VALUE)
            {
                char buffer[100]{};

                SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER p{};
                p.spt.Length             = sizeof (SCSI_PASS_THROUGH);
                p.spt.CdbLength          = 6;
                p.spt.SenseInfoLength    = 24;
                p.spt.DataIn             = SCSI_IOCTL_DATA_IN;
                p.spt.DataTransferLength = sizeof (buffer);
                p.spt.TimeOutValue       = 2;
                p.spt.DataBuffer         = buffer;
                p.spt.SenseInfoOffset    = offsetof (SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER, ucSenseBuf);
                p.spt.Cdb[0]             = 0x12;
                p.spt.Cdb[4]             = 100;

                DWORD bytesReturned = 0;

                if (DeviceIoControl (h, IOCTL_SCSI_PASS_THROUGH_DIRECT,
                                     &p, sizeof (p), &p, sizeof (p),
                                     &bytesReturned, 0) != 0)
                {
                    CDDeviceDescription dev;
                    dev.scsiDriveLetter = driveLetter;
                    dev.createDescription (buffer);

                    SCSI_ADDRESS scsiAddr{};
                    scsiAddr.Length = sizeof (scsiAddr);

                    if (DeviceIoControl (h, IOCTL_SCSI_GET_ADDRESS,
                                         0, 0, &scsiAddr, sizeof (scsiAddr),
                                         &bytesReturned, 0) != 0)
                    {
                        dev.ha = scsiAddr.PortNumber;
                        dev.tgt = scsiAddr.TargetId;
                        dev.lun = scsiAddr.Lun;
                        list.add (dev);
                    }
                }

                CloseHandle (h);
            }
        }
    }
}

DWORD performScsiPassThroughCommand (SRB_ExecSCSICmd* const srb, const char driveLetter,
                                     HANDLE& deviceHandle, const bool retryOnFailure)
{
    SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER s{};
    s.spt.Length = sizeof (SCSI_PASS_THROUGH);
    s.spt.CdbLength = srb->SRB_CDBLen;

    s.spt.DataIn = (BYTE) ((srb->SRB_Flags & SRB_DIR_IN)
                            ? SCSI_IOCTL_DATA_IN
                            : ((srb->SRB_Flags & SRB_DIR_OUT)
                                ? SCSI_IOCTL_DATA_OUT
                                : SCSI_IOCTL_DATA_UNSPECIFIED));

    s.spt.DataTransferLength = srb->SRB_BufLen;
    s.spt.TimeOutValue = 5;
    s.spt.DataBuffer = srb->SRB_BufPointer;
    s.spt.SenseInfoOffset = offsetof (SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER, ucSenseBuf);

    memcpy (s.spt.Cdb, srb->CDBByte, srb->SRB_CDBLen);

    srb->SRB_Status = SS_ERR;
    srb->SRB_TargStat = 0x0004;

    DWORD bytesReturned = 0;

    if (DeviceIoControl (deviceHandle, IOCTL_SCSI_PASS_THROUGH_DIRECT,
                         &s, sizeof (s), &s, sizeof (s), &bytesReturned, 0) != 0)
    {
        srb->SRB_Status = SS_COMP;
    }
    else if (retryOnFailure)
    {
        const DWORD error = GetLastError();

        if ((error == ERROR_MEDIA_CHANGED) || (error == ERROR_INVALID_HANDLE))
        {
            if (error != ERROR_INVALID_HANDLE)
                CloseHandle (deviceHandle);

            deviceHandle = createSCSIDeviceHandle (driveLetter);

            return performScsiPassThroughCommand (srb, driveLetter, deviceHandle, false);
        }
    }

    return srb->SRB_Status;
}


//==============================================================================
// Controller types..

class ControllerType1 final : public CDController
{
public:
    ControllerType1() {}

    bool read (CDReadBuffer& rb)
    {
        if (rb.numFrames * 2352 > rb.bufferSize)
            return false;

        SRB_ExecSCSICmd s;
        prepare (s);
        s.SRB_Flags = SRB_DIR_IN | SRB_EVENT_NOTIFY;
        s.SRB_BufLen = rb.bufferSize;
        s.SRB_BufPointer = rb.buffer;
        s.SRB_CDBLen = 12;
        s.CDBByte[0] = 0xBE;
        s.CDBByte[3] = (BYTE) ((rb.startFrame >> 16) & 0xFF);
        s.CDBByte[4] = (BYTE) ((rb.startFrame >> 8) & 0xFF);
        s.CDBByte[5] = (BYTE) (rb.startFrame & 0xFF);
        s.CDBByte[8] = (BYTE) (rb.numFrames & 0xFF);
        s.CDBByte[9] = (BYTE) (deviceInfo->readType == READTYPE_ATAPI1 ? 0x10 : 0xF0);
        perform (s);

        if (s.SRB_Status != SS_COMP)
            return false;

        rb.dataLength = rb.numFrames * 2352;
        rb.dataStartOffset = 0;
        return true;
    }
};

//==============================================================================
class ControllerType2 final : public CDController
{
public:
    ControllerType2() {}

    void shutDown()
    {
        if (initialised)
        {
            BYTE bufPointer[] = { 0, 0, 0, 8, 83, 0, 0, 0, 0, 0, 8, 0 };

            SRB_ExecSCSICmd s;
            prepare (s);
            s.SRB_Flags = SRB_EVENT_NOTIFY | SRB_ENABLE_RESIDUAL_COUNT;
            s.SRB_BufLen = 0x0C;
            s.SRB_BufPointer = bufPointer;
            s.SRB_CDBLen = 6;
            s.CDBByte[0] = 0x15;
            s.CDBByte[4] = 0x0C;
            perform (s);
        }
    }

    bool init()
    {
        SRB_ExecSCSICmd s;
        s.SRB_Status = SS_ERR;

        if (deviceInfo->readType == READTYPE_READ10_2)
        {
            BYTE bufPointer1[] = { 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 9, 48, 35, 6, 0, 0, 0, 0, 0, 128 };
            BYTE bufPointer2[] = { 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 9, 48, 1, 6, 32, 7, 0, 0, 0, 0 };

            for (int i = 0; i < 2; ++i)
            {
                prepare (s);
                s.SRB_Flags = SRB_EVENT_NOTIFY;
                s.SRB_BufLen = 0x14;
                s.SRB_BufPointer = (i == 0) ? bufPointer1 : bufPointer2;
                s.SRB_CDBLen = 6;
                s.CDBByte[0] = 0x15;
                s.CDBByte[1] = 0x10;
                s.CDBByte[4] = 0x14;
                perform (s);

                if (s.SRB_Status != SS_COMP)
                    return false;
            }
        }
        else
        {
            BYTE bufPointer[] = { 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 9, 48 };

            prepare (s);
            s.SRB_Flags = SRB_EVENT_NOTIFY;
            s.SRB_BufLen = 0x0C;
            s.SRB_BufPointer = bufPointer;
            s.SRB_CDBLen = 6;
            s.CDBByte[0] = 0x15;
            s.CDBByte[4] = 0x0C;
            perform (s);
        }

        return s.SRB_Status == SS_COMP;
    }

    bool read (CDReadBuffer& rb)
    {
        if (rb.numFrames * 2352 > rb.bufferSize)
            return false;

        if (! initialised)
        {
            initialised = init();

            if (! initialised)
                return false;
        }

        SRB_ExecSCSICmd s;
        prepare (s);
        s.SRB_Flags = SRB_DIR_IN | SRB_EVENT_NOTIFY;
        s.SRB_BufLen = rb.bufferSize;
        s.SRB_BufPointer = rb.buffer;
        s.SRB_CDBLen = 10;
        s.CDBByte[0] = 0x28;
        s.CDBByte[1] = (BYTE) (deviceInfo->info.lun << 5);
        s.CDBByte[3] = (BYTE) ((rb.startFrame >> 16) & 0xFF);
        s.CDBByte[4] = (BYTE) ((rb.startFrame >> 8) & 0xFF);
        s.CDBByte[5] = (BYTE) (rb.startFrame & 0xFF);
        s.CDBByte[8] = (BYTE) (rb.numFrames & 0xFF);
        perform (s);

        if (s.SRB_Status != SS_COMP)
            return false;

        rb.dataLength = rb.numFrames * 2352;
        rb.dataStartOffset = 0;
        return true;
    }
};

//==============================================================================
class ControllerType3 final : public CDController
{
public:
    ControllerType3() {}

    bool read (CDReadBuffer& rb)
    {
        if (rb.numFrames * 2352 > rb.bufferSize)
            return false;

        if (! initialised)
        {
            setPaused (false);
            initialised = true;
        }

        SRB_ExecSCSICmd s;
        prepare (s);
        s.SRB_Flags = SRB_DIR_IN | SRB_EVENT_NOTIFY;
        s.SRB_BufLen = rb.numFrames * 2352;
        s.SRB_BufPointer = rb.buffer;
        s.SRB_CDBLen = 12;
        s.CDBByte[0] = 0xD8;
        s.CDBByte[3] = (BYTE) ((rb.startFrame >> 16) & 0xFF);
        s.CDBByte[4] = (BYTE) ((rb.startFrame >> 8) & 0xFF);
        s.CDBByte[5] = (BYTE) (rb.startFrame & 0xFF);
        s.CDBByte[9] = (BYTE) (rb.numFrames & 0xFF);
        perform (s);

        if (s.SRB_Status != SS_COMP)
            return false;

        rb.dataLength = rb.numFrames * 2352;
        rb.dataStartOffset = 0;
        return true;
    }
};

//==============================================================================
class ControllerType4 final : public CDController
{
public:
    ControllerType4() {}

    bool selectD4Mode()
    {
        BYTE bufPointer[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 48 };

        SRB_ExecSCSICmd s;
        prepare (s);
        s.SRB_Flags = SRB_EVENT_NOTIFY;
        s.SRB_CDBLen = 6;
        s.SRB_BufLen = 12;
        s.SRB_BufPointer = bufPointer;
        s.CDBByte[0] = 0x15;
        s.CDBByte[1] = 0x10;
        s.CDBByte[4] = 0x08;
        perform (s);

        return s.SRB_Status == SS_COMP;
    }

    bool read (CDReadBuffer& rb)
    {
        if (rb.numFrames * 2352 > rb.bufferSize)
            return false;

        if (! initialised)
        {
            setPaused (true);

            if (deviceInfo->readType == READTYPE_READ_D4_1)
                selectD4Mode();

            initialised = true;
        }

        SRB_ExecSCSICmd s;
        prepare (s);
        s.SRB_Flags = SRB_DIR_IN | SRB_EVENT_NOTIFY;
        s.SRB_BufLen = rb.bufferSize;
        s.SRB_BufPointer = rb.buffer;
        s.SRB_CDBLen = 10;
        s.CDBByte[0] = 0xD4;
        s.CDBByte[3] = (BYTE) ((rb.startFrame >> 16) & 0xFF);
        s.CDBByte[4] = (BYTE) ((rb.startFrame >> 8) & 0xFF);
        s.CDBByte[5] = (BYTE) (rb.startFrame & 0xFF);
        s.CDBByte[8] = (BYTE) (rb.numFrames & 0xFF);
        perform (s);

        if (s.SRB_Status != SS_COMP)
            return false;

        rb.dataLength = rb.numFrames * 2352;
        rb.dataStartOffset = 0;
        return true;
    }
};


//==============================================================================
void CDController::prepare (SRB_ExecSCSICmd& s)
{
    zerostruct (s);
    s.SRB_Cmd = SC_EXEC_SCSI_CMD;
    s.SRB_HaID = deviceInfo->info.ha;
    s.SRB_Target = deviceInfo->info.tgt;
    s.SRB_Lun = deviceInfo->info.lun;
    s.SRB_SenseLen = SENSE_LEN;
}

void CDController::perform (SRB_ExecSCSICmd& s)
{
    s.SRB_PostProc = CreateEvent (0, TRUE, FALSE, 0);

    deviceInfo->performScsiCommand (s.SRB_PostProc, s);
}

void CDController::setPaused (bool paused)
{
    SRB_ExecSCSICmd s;
    prepare (s);
    s.SRB_Flags = SRB_EVENT_NOTIFY;
    s.SRB_CDBLen = 10;
    s.CDBByte[0] = 0x4B;
    s.CDBByte[8] = (BYTE) (paused ? 0 : 1);
    perform (s);
}

bool CDController::readAudio (CDReadBuffer& rb, CDReadBuffer* overlapBuffer)
{
    if (overlapBuffer != nullptr)
    {
        const bool canDoJitter = (overlapBuffer->bufferSize >= 2352 * framesToCheck);
        const bool doJitter = canDoJitter && ! overlapBuffer->isZero();

        if (doJitter
             && overlapBuffer->startFrame > 0
             && overlapBuffer->numFrames > 0
             && overlapBuffer->dataLength > 0)
        {
            const int numFrames = rb.numFrames;

            if (overlapBuffer->startFrame == (rb.startFrame - framesToCheck))
            {
                rb.startFrame -= framesOverlap;

                if (framesToCheck < framesOverlap
                     && numFrames + framesOverlap <= rb.bufferSize / 2352)
                    rb.numFrames += framesOverlap;
            }
            else
            {
                overlapBuffer->dataLength = 0;
                overlapBuffer->startFrame = 0;
                overlapBuffer->numFrames = 0;
            }
        }

        if (! read (rb))
            return false;

        if (doJitter)
        {
            const int checkLen = framesToCheck * 2352;
            const int maxToCheck = rb.dataLength - checkLen;

            if (overlapBuffer->dataLength == 0 || overlapBuffer->isZero())
                return true;

            BYTE* const p = overlapBuffer->buffer + overlapBuffer->dataStartOffset;
            bool found = false;

            for (int i = 0; i < maxToCheck; ++i)
            {
                if (memcmp (p, rb.buffer + i, checkLen) == 0)
                {
                    i += checkLen;
                    rb.dataStartOffset = i;
                    rb.dataLength -= i;
                    rb.startFrame = overlapBuffer->startFrame + framesToCheck;
                    found = true;
                    break;
                }
            }

            rb.numFrames = rb.dataLength / 2352;
            rb.dataLength = 2352 * rb.numFrames;

            if (! found)
                return false;
        }

        if (canDoJitter)
        {
            memcpy (overlapBuffer->buffer,
                    rb.buffer + rb.dataStartOffset + 2352 * (rb.numFrames - framesToCheck),
                    2352 * framesToCheck);

            overlapBuffer->startFrame = rb.startFrame + rb.numFrames - framesToCheck;
            overlapBuffer->numFrames = framesToCheck;
            overlapBuffer->dataLength = 2352 * framesToCheck;
            overlapBuffer->dataStartOffset = 0;
        }
        else
        {
            overlapBuffer->startFrame = 0;
            overlapBuffer->numFrames = 0;
            overlapBuffer->dataLength = 0;
        }

        return true;
    }

    return read (rb);
}

int CDController::getLastIndex()
{
    char qdata[100];

    SRB_ExecSCSICmd s;
    prepare (s);
    s.SRB_Flags = SRB_DIR_IN | SRB_EVENT_NOTIFY;
    s.SRB_BufLen = sizeof (qdata);
    s.SRB_BufPointer = (BYTE*) qdata;
    s.SRB_CDBLen = 12;
    s.CDBByte[0] = 0x42;
    s.CDBByte[1] = (BYTE) (deviceInfo->info.lun << 5);
    s.CDBByte[2] = 64;
    s.CDBByte[3] = 1; // get current position
    s.CDBByte[7] = 0;
    s.CDBByte[8] = (BYTE) sizeof (qdata);
    perform (s);

    return s.SRB_Status == SS_COMP ? qdata[7] : 0;
}

//==============================================================================
bool CDDeviceHandle::readTOC (TOC* lpToc)
{
    SRB_ExecSCSICmd s{};
    s.SRB_Cmd = SC_EXEC_SCSI_CMD;
    s.SRB_HaID = info.ha;
    s.SRB_Target = info.tgt;
    s.SRB_Lun = info.lun;
    s.SRB_Flags = SRB_DIR_IN | SRB_EVENT_NOTIFY;
    s.SRB_BufLen = 0x324;
    s.SRB_BufPointer = (BYTE*) lpToc;
    s.SRB_SenseLen = 0x0E;
    s.SRB_CDBLen = 0x0A;
    s.SRB_PostProc = CreateEvent (0, TRUE, FALSE, 0);
    s.CDBByte[0] = 0x43;
    s.CDBByte[1] = 0x00;
    s.CDBByte[7] = 0x03;
    s.CDBByte[8] = 0x24;

    performScsiCommand (s.SRB_PostProc, s);
    return (s.SRB_Status == SS_COMP);
}

void CDDeviceHandle::performScsiCommand (HANDLE event, SRB_ExecSCSICmd& s)
{
    ResetEvent (event);
    DWORD status = performScsiPassThroughCommand ((SRB_ExecSCSICmd*) &s, info.scsiDriveLetter, scsiHandle, true);

    if (status == SS_PENDING)
        WaitForSingleObject (event, 4000);

    CloseHandle (event);
}

bool CDDeviceHandle::readAudio (CDReadBuffer& buffer, CDReadBuffer* overlapBuffer)
{
    if (controller == 0)
    {
           testController (READTYPE_ATAPI2,    new ControllerType1(), buffer)
        || testController (READTYPE_ATAPI1,    new ControllerType1(), buffer)
        || testController (READTYPE_READ10_2,  new ControllerType2(), buffer)
        || testController (READTYPE_READ10,    new ControllerType2(), buffer)
        || testController (READTYPE_READ_D8,   new ControllerType3(), buffer)
        || testController (READTYPE_READ_D4,   new ControllerType4(), buffer)
        || testController (READTYPE_READ_D4_1, new ControllerType4(), buffer);
    }

    buffer.index = 0;

    if (controller != nullptr && controller->readAudio (buffer, overlapBuffer))
    {
        if (buffer.wantsIndex)
            buffer.index = controller->getLastIndex();

        return true;
    }

    return false;
}

void CDDeviceHandle::openDrawer (bool shouldBeOpen)
{
    if (shouldBeOpen)
    {
        if (controller != nullptr)
        {
            controller->shutDown();
            controller = nullptr;
        }

        if (scsiHandle != 0)
        {
            CloseHandle (scsiHandle);
            scsiHandle = 0;
        }
    }

    SRB_ExecSCSICmd s{};
    s.SRB_Cmd = SC_EXEC_SCSI_CMD;
    s.SRB_HaID = info.ha;
    s.SRB_Target = info.tgt;
    s.SRB_Lun = info.lun;
    s.SRB_SenseLen = SENSE_LEN;
    s.SRB_Flags = SRB_DIR_IN | SRB_EVENT_NOTIFY;
    s.SRB_BufLen = 0;
    s.SRB_BufPointer = 0;
    s.SRB_CDBLen = 12;
    s.CDBByte[0] = 0x1b;
    s.CDBByte[1] = (BYTE) (info.lun << 5);
    s.CDBByte[4] = (BYTE) (shouldBeOpen ? 2 : 3);
    s.SRB_PostProc = CreateEvent (0, TRUE, FALSE, 0);

    performScsiCommand (s.SRB_PostProc, s);
}

bool CDDeviceHandle::testController (const int type, CDController* const newController, CDReadBuffer& rb)
{
    controller.reset (newController);
    readType = (BYTE) type;

    controller->deviceInfo = this;
    controller->framesToCheck = 1;
    controller->framesOverlap = 3;

    bool passed = false;
    memset (rb.buffer, 0xcd, rb.bufferSize);

    if (controller->read (rb))
    {
        passed = true;
        int* p = (int*) (rb.buffer + rb.dataStartOffset);
        int wrong = 0;

        for (int i = rb.dataLength / 4; --i >= 0;)
        {
            if (*p++ == (int) 0xcdcdcdcd)
            {
                if (++wrong == 4)
                {
                    passed = false;
                    break;
                }
            }
            else
            {
                wrong = 0;
            }
        }
    }

    if (! passed)
    {
        controller->shutDown();
        controller = nullptr;
    }

    return passed;
}


//==============================================================================
struct CDDeviceWrapper
{
    CDDeviceWrapper (const CDDeviceDescription& device, HANDLE scsiHandle)
        : deviceHandle (device, scsiHandle), overlapBuffer (3), jitter (false)
    {
        // xxx jitter never seemed to actually be enabled (??)
    }

    CDDeviceHandle deviceHandle;
    CDReadBuffer overlapBuffer;
    bool jitter;
};

//==============================================================================
int getAddressOfTrack (const TOCTRACK& t) noexcept
{
    return (((DWORD) t.addr[0]) << 24) + (((DWORD) t.addr[1]) << 16)
            + (((DWORD) t.addr[2]) << 8) + ((DWORD) t.addr[3]);
}

const int samplesPerFrame = 44100 / 75;
const int bytesPerFrame = samplesPerFrame * 4;
const int framesPerIndexRead = 4;

}

//==============================================================================
StringArray AudioCDReader::getAvailableCDNames()
{
    using namespace CDReaderHelpers;
    StringArray results;

    Array<CDDeviceDescription> list;
    findCDDevices (list);

    for (int i = 0; i < list.size(); ++i)
    {
        String s;
        if (list[i].scsiDriveLetter > 0)
            s << String::charToString (list[i].scsiDriveLetter).toUpperCase() << ": ";

        s << list[i].description;
        results.add (s);
    }

    return results;
}

AudioCDReader* AudioCDReader::createReaderForCD (const int deviceIndex)
{
    using namespace CDReaderHelpers;

    Array<CDDeviceDescription> list;
    findCDDevices (list);

    if (isPositiveAndBelow (deviceIndex, list.size()))
    {
        HANDLE h = createSCSIDeviceHandle (list [deviceIndex].scsiDriveLetter);

        if (h != INVALID_HANDLE_VALUE)
        {
            std::unique_ptr<AudioCDReader> cd (new AudioCDReader (new CDDeviceWrapper (list [deviceIndex], h)));

            if (cd->lengthInSamples > 0)
                return cd.release();
        }
    }

    return nullptr;
}

AudioCDReader::AudioCDReader (void* handle_)
    : AudioFormatReader (0, "CD Audio"),
      handle (handle_),
      indexingEnabled (false),
      lastIndex (0),
      firstFrameInBuffer (0),
      samplesInBuffer (0)
{
    using namespace CDReaderHelpers;
    jassert (handle_ != nullptr);

    refreshTrackLengths();

    sampleRate = 44100.0;
    bitsPerSample = 16;
    numChannels = 2;
    usesFloatingPointData = false;

    buffer.setSize (4 * bytesPerFrame, true);
}

AudioCDReader::~AudioCDReader()
{
    using namespace CDReaderHelpers;
    CDDeviceWrapper* const device = static_cast<CDDeviceWrapper*> (handle);
    delete device;
}

bool AudioCDReader::readSamples (int* const* destSamples, int numDestChannels, int startOffsetInDestBuffer,
                                 int64 startSampleInFile, int numSamples)
{
    using namespace CDReaderHelpers;
    CDDeviceWrapper* const device = static_cast<CDDeviceWrapper*> (handle);

    bool ok = true;

    while (numSamples > 0)
    {
        const int bufferStartSample = firstFrameInBuffer * samplesPerFrame;
        const int bufferEndSample = bufferStartSample + samplesInBuffer;

        if (startSampleInFile >= bufferStartSample
             && startSampleInFile < bufferEndSample)
        {
            const int toDo = (int) jmin ((int64) numSamples, bufferEndSample - startSampleInFile);

            int* const l = destSamples[0] + startOffsetInDestBuffer;
            int* const r = numDestChannels > 1 ? (destSamples[1] + startOffsetInDestBuffer) : nullptr;
            const short* src = (const short*) buffer.getData();
            src += 2 * (startSampleInFile - bufferStartSample);

            for (int i = 0; i < toDo; ++i)
            {
                l[i] = src [i << 1] << 16;

                if (r != nullptr)
                    r[i] = src [(i << 1) + 1] << 16;
            }

            startOffsetInDestBuffer += toDo;
            startSampleInFile += toDo;
            numSamples -= toDo;
        }
        else
        {
            const int framesInBuffer = (int) (buffer.getSize() / bytesPerFrame);
            const int frameNeeded = (int) (startSampleInFile / samplesPerFrame);

            if (firstFrameInBuffer + framesInBuffer != frameNeeded)
            {
                device->overlapBuffer.dataLength = 0;
                device->overlapBuffer.startFrame = 0;
                device->overlapBuffer.numFrames = 0;
                device->jitter = false;
            }

            firstFrameInBuffer = frameNeeded;
            lastIndex = 0;

            CDReadBuffer readBuffer (framesInBuffer + 4);
            readBuffer.wantsIndex = indexingEnabled;

            int i;
            for (i = 5; --i >= 0;)
            {
                readBuffer.startFrame = frameNeeded;
                readBuffer.numFrames = framesInBuffer;

                if (device->deviceHandle.readAudio (readBuffer, device->jitter ? &device->overlapBuffer : 0))
                    break;
                else
                    device->overlapBuffer.dataLength = 0;
            }

            if (i >= 0)
            {
                buffer.copyFrom (readBuffer.buffer + readBuffer.dataStartOffset, 0, readBuffer.dataLength);
                samplesInBuffer = readBuffer.dataLength >> 2;
                lastIndex = readBuffer.index;
            }
            else
            {
                int* l = destSamples[0] + startOffsetInDestBuffer;
                int* r = numDestChannels > 1 ? (destSamples[1] + startOffsetInDestBuffer) : nullptr;

                while (--numSamples >= 0)
                {
                    *l++ = 0;

                    if (r != nullptr)
                        *r++ = 0;
                }

                // sometimes the read fails for just the very last couple of blocks, so
                // we'll ignore and errors in the last half-second of the disk..
                ok = startSampleInFile > (trackStartSamples [getNumTracks()] - 20000);
                break;
            }
        }
    }

    return ok;
}

bool AudioCDReader::isCDStillPresent() const
{
    using namespace CDReaderHelpers;
    TOC toc{};
    return static_cast<CDDeviceWrapper*> (handle)->deviceHandle.readTOC (&toc);
}

void AudioCDReader::refreshTrackLengths()
{
    using namespace CDReaderHelpers;
    trackStartSamples.clear();
    zeromem (audioTracks, sizeof (audioTracks));

    TOC toc{};

    if (static_cast<CDDeviceWrapper*> (handle)->deviceHandle.readTOC (&toc))
    {
        int numTracks = 1 + toc.lastTrack - toc.firstTrack;

        for (int i = 0; i <= numTracks; ++i)
        {
            trackStartSamples.add (samplesPerFrame * getAddressOfTrack (toc.tracks [i]));
            audioTracks [i] = ((toc.tracks[i].ADR & 4) == 0);
        }
    }

    lengthInSamples = getPositionOfTrackStart (getNumTracks());
}

bool AudioCDReader::isTrackAudio (int trackNum) const
{
    return trackNum >= 0 && trackNum < getNumTracks() && audioTracks [trackNum];
}

void AudioCDReader::enableIndexScanning (bool b)
{
    indexingEnabled = b;
}

int AudioCDReader::getLastIndex() const
{
    return lastIndex;
}

int AudioCDReader::getIndexAt (int samplePos)
{
    using namespace CDReaderHelpers;
    auto* device = static_cast<CDDeviceWrapper*> (handle);

    const int frameNeeded = samplePos / samplesPerFrame;

    device->overlapBuffer.dataLength = 0;
    device->overlapBuffer.startFrame = 0;
    device->overlapBuffer.numFrames = 0;
    device->jitter = false;

    firstFrameInBuffer = 0;
    lastIndex = 0;

    CDReadBuffer readBuffer (4 + framesPerIndexRead);
    readBuffer.wantsIndex = true;

    int i;
    for (i = 5; --i >= 0;)
    {
        readBuffer.startFrame = frameNeeded;
        readBuffer.numFrames = framesPerIndexRead;

        if (device->deviceHandle.readAudio (readBuffer))
            break;
    }

    if (i >= 0)
        return readBuffer.index;

    return -1;
}

Array<int> AudioCDReader::findIndexesInTrack (const int trackNumber)
{
    using namespace CDReaderHelpers;
    Array<int> indexes;

    const int trackStart = getPositionOfTrackStart (trackNumber);
    const int trackEnd = getPositionOfTrackStart (trackNumber + 1);

    bool needToScan = true;

    if (trackEnd - trackStart > 20 * 44100)
    {
        // check the end of the track for indexes before scanning the whole thing
        needToScan = false;
        int pos = jmax (trackStart, trackEnd - 44100 * 5);
        bool seenAnIndex = false;

        while (pos <= trackEnd - samplesPerFrame)
        {
            const int index = getIndexAt (pos);

            if (index == 0)
            {
                // lead-out, so skip back a bit if we've not found any indexes yet..
                if (seenAnIndex)
                    break;

                pos -= 44100 * 5;

                if (pos < trackStart)
                    break;
            }
            else
            {
                if (index > 0)
                    seenAnIndex = true;

                if (index > 1)
                {
                    needToScan = true;
                    break;
                }

                pos += samplesPerFrame * framesPerIndexRead;
            }
        }
    }

    if (needToScan)
    {
        auto* device = static_cast<CDDeviceWrapper*> (handle);

        int pos = trackStart;
        int last = -1;

        while (pos < trackEnd - samplesPerFrame * 10)
        {
            const int frameNeeded = pos / samplesPerFrame;

            device->overlapBuffer.dataLength = 0;
            device->overlapBuffer.startFrame = 0;
            device->overlapBuffer.numFrames = 0;
            device->jitter = false;

            firstFrameInBuffer = 0;

            CDReadBuffer readBuffer (4);
            readBuffer.wantsIndex = true;

            int i;
            for (i = 5; --i >= 0;)
            {
                readBuffer.startFrame = frameNeeded;
                readBuffer.numFrames = framesPerIndexRead;

                if (device->deviceHandle.readAudio (readBuffer))
                    break;
            }

            if (i < 0)
                break;

            if (readBuffer.index > last && readBuffer.index > 1)
            {
                last = readBuffer.index;
                indexes.add (pos);
            }

            pos += samplesPerFrame * framesPerIndexRead;
        }

        indexes.removeFirstMatchingValue (trackStart);
    }

    return indexes;
}

void AudioCDReader::ejectDisk()
{
    using namespace CDReaderHelpers;
    static_cast<CDDeviceWrapper*> (handle)->deviceHandle.openDrawer (true);
}

} // namespace juce
